/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nageoffer.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.lang.hash.MurmurHash;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.project.common.convention.exception.ClientException;
import com.nageoffer.shortlink.project.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.project.common.enums.VailDateTypeEnum;
import com.nageoffer.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.nageoffer.shortlink.project.dao.entity.ShortLinkDO;
import com.nageoffer.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkMapper;
import com.nageoffer.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.project.dto.resp.*;
import com.nageoffer.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.nageoffer.shortlink.project.service.ShortLinkService;
import com.nageoffer.shortlink.project.toolkit.BloomFilterIdxUtil;
import com.nageoffer.shortlink.project.toolkit.HashUtil;
import com.nageoffer.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {


    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Transactional(rollbackFor = Exception.class)
@Override
public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
    // 验证URL白名单
    verificationWhitelist(requestParam.getOriginUrl());

    // 生成短链接后缀
    String shortLinkSuffix = generateSuffix(requestParam);

    // 构建完整的短链接URL
    String fullShortUrl = String.format("%s/%s",
            createShortLinkDefaultDomain,
            shortLinkSuffix);

    // 插入前检查是否已存在（防并发）
    if (shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                .eq(ShortLinkDO::getDelFlag, 0);
        if (baseMapper.exists(queryWrapper)) {
            throw new ServiceException(String.format("短链接：%s 已存在", fullShortUrl));
        }
    }

    // 构建短链接数据对象
    ShortLinkDO shortLinkDO = ShortLinkDO.builder()
            .domain(createShortLinkDefaultDomain)
            .originUrl(requestParam.getOriginUrl())
            .gid(requestParam.getGid())
            .createdType(requestParam.getCreatedType())
            .validDateType(requestParam.getValidDateType())
            .validDate(requestParam.getValidDate())
            .describe(requestParam.getDescribe())
            .shortUri(shortLinkSuffix)
            .enableStatus(0)
            .totalPv(0)
            .totalUv(0)
            .totalUip(0)
            .delTime(0L)
            .fullShortUrl(fullShortUrl)
            .favicon(getFavicon(requestParam.getOriginUrl()))
            .build();

    // 构建跳转数据对象
    ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
            .fullShortUrl(fullShortUrl)
            .gid(requestParam.getGid())
            .build();

    // 添加到布隆过滤器
    addShortLinkToBloomFilters(fullShortUrl);

    try {
        baseMapper.insert(shortLinkDO);
        shortLinkGotoMapper.insert(linkGotoDO);
    } catch (DuplicateKeyException ex) {
        if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
            shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        }
        throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
    }

    // 缓存预热
    stringRedisTemplate.opsForValue().set(
            String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
            requestParam.getOriginUrl(),
            LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
    );

    // 添加到缓存穿透布隆过滤器
    shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);

    // 返回响应
    return ShortLinkCreateRespDTO.builder()
            .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
            .originUrl(requestParam.getOriginUrl())
            .gid(requestParam.getGid())
            .build();
}

private void addShortLinkToBloomFilters(String shortLink) {
    int bloomFilterSize = Optional.ofNullable(stringRedisTemplate.opsForValue().get("sharding-expansion:bloom_filter_size"))
            .map(Integer::parseInt)
            .orElse(1000000); // 默认大小

    int hash32 = MurmurHash.hash32(shortLink);
    long mod = hash32 < 0 ? Integer.MAX_VALUE - (long) hash32 : hash32;
    long idx = mod % bloomFilterSize;

    String bloomFilterCurrentIndex = stringRedisTemplate.opsForValue().get("sharding-expansion:bloom_filter_current_index");
    String bloomFilterName = String.format("sharding-expansion:short-link-create-bloom-filter-%s-%d", bloomFilterCurrentIndex, idx);
    RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(bloomFilterName);
    bloomFilter.add(shortLink);

    String expansionFlag = stringRedisTemplate.opsForValue().get("sharding-expansion:expansion_flag");
    if ("true".equals(expansionFlag)) {
        long expansionIdxLong = mod % calculateExpansionSize(bloomFilterSize);
        String expansionIdxStr = BloomFilterIdxUtil.reversal(bloomFilterCurrentIndex);
        String expansionBloomFilterName = String.format("sharding-expansion:short-link-create-bloom-filter-%s-%d", expansionIdxStr, expansionIdxLong);
        RBloomFilter<String> expansionBloomFilter = redissonClient.getBloomFilter(expansionBloomFilterName);
        expansionBloomFilter.add(shortLink);
    }
}


    private long calculateExpansionSize(int bloomFilterSize) {
        return (long) (bloomFilterSize * 1.5);
    }

    /**
     * 使用分布式锁创建短链接
     * 此方法首先验证白名单，然后使用分布式锁确保短链接的生成是原子操作
     * 通过布隆过滤器检查来优化性能，详情见：https://nageoffer.com/shortlink/question
     * 
     * @param requestParam 短链接创建请求参数
     * @return 短链接创建响应DTO
     */
    @Override
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO requestParam) {
        // 验证白名单
        verificationWhitelist(requestParam.getOriginUrl());
        String fullShortUrl;
        // 为什么说布隆过滤器性能远胜于分布式锁？详情查看：https://nageoffer.com/shortlink/question
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            // 在持有锁的情况下生成短链接后缀，确保并发安全
            String shortLinkSuffix = generateSuffixByLock(requestParam);
            // 构建完整的短链接URL
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();
            // 创建短链接数据对象
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(requestParam.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .shortUri(shortLinkSuffix)
                    .enableStatus(0)
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .fullShortUrl(fullShortUrl)
                    .favicon(getFavicon(requestParam.getOriginUrl()))
                    .build();
            // 创建短链接跳转数据对象
            ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(requestParam.getGid())
                    .build();
            // 尝试插入短链接和跳转记录到数据库
            try {
                baseMapper.insert(shortLinkDO);
                shortLinkGotoMapper.insert(linkGotoDO);
            } catch (DuplicateKeyException ex) {
                // 处理重复键异常
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }
            // 将短链接和原始URL的映射关系存入Redis，设置过期时间
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    requestParam.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
            );
        } finally {
            // 释放锁
            lock.unlock();
        }
        // 返回短链接创建响应信息
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    /**
     * 批量创建短链接*
     * 该方法接收一个包含多个原始URL和描述的请求对象，为每个原始URL生成一个短链接，并返回所有短链接的列表
     * 如果在创建短链接过程中遇到任何错误，该方法会记录错误信息，并继续处理剩余的URL
     * 
     * @param requestParam 包含原始URL列表和描述列表的请求对象
     * @return 包含所有成功创建的短链接信息的响应对象
     */
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        // 获取原始URL列表和描述列表
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        
        // 初始化结果列表
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        
        // 遍历原始URL列表，为每个URL创建一个短链接
        for (int i = 0; i < originUrls.size(); i++) {
            // 将请求参数转换为创建短链接所需的对象
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            // 设置当前URL和描述
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            
            try {
                // 创建短链接
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                
                // 构建短链接信息对象并添加到结果列表
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                // 如果创建失败，记录错误信息
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        
        // 构建并返回批量创建短链接的响应对象
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    /**
     * 更新短链接信息
     * 此方法主要用于更新短链接的相关信息，包括原始URL、有效期等
     * 使用@Transactional注解确保操作的原子性，任何步骤失败都将回滚更改
     * 
     * @param requestParam 短链接更新请求参数对象，包含需要更新的短链接信息
     * @throws ClientException 如果短链接记录不存在或更新失败时抛出客户端异常
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 验证原始URL是否在白名单中
        verificationWhitelist(requestParam.getOriginUrl());
        
        // 查询短链接是否存在
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        
        // 如果短链接不存在，抛出异常
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }
        
        // 如果GID匹配，直接更新短链接信息
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(Objects.equals(requestParam.getOriginUrl(), hasShortLinkDO.getOriginUrl()) ? hasShortLinkDO.getFavicon() : getFavicon(requestParam.getOriginUrl()))
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();
            
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            // 使用分布式锁确保数据一致性
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();
            rLock.lock();
            try {
                // 更新现有短链接的删除状态
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);
                
                // 插入新的短链接记录
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(Objects.equals(requestParam.getOriginUrl(), hasShortLinkDO.getOriginUrl()) ? hasShortLinkDO.getFavicon() : getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);
                
                // 更新短链接跳转表
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.delete(linkGotoQueryWrapper);
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);
            } finally {
                rLock.unlock();
            }
        }
        
        // 确保缓存和数据库的一致性
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())
                || !Objects.equals(hasShortLinkDO.getOriginUrl(), requestParam.getOriginUrl())) {
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            Date currentDate = new Date();
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(currentDate)) {
                if (Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) || requestParam.getValidDate().after(currentDate)) {
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    /**
     * 根据请求参数分页查询短链接信息
     * 此方法使用MyBatis-Plus的分页查询功能，将查询结果转换为DTO并格式化域名
     * 
     * @param requestParam 短链接分页查询请求参数，包含分页信息和查询条件
     * @return IPage&lt;ShortLinkPageRespDTO&gt; 分页查询结果，包含短链接信息的列表和分页数据
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        // 执行分页查询，获取短链接数据对象列表
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        
        // 将查询结果转换为ShortLinkPageRespDTO对象，并格式化域名
        return resultPage.convert(each -> {
            // 将ShortLinkDO对象转换为ShortLinkPageRespDTO对象
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            
            // 为域名添加HTTP协议前缀，以便统一展示格式
            result.setDomain("http://" + result.getDomain());
            
            // 返回转换并格式化后的对象
            return result;
        });
    }

    /**
     * 列出短链接分组查询响应DTO列表
     * 此方法根据提供的分组ID列表查询每个分组下的短链接数量
     * 它通过构造一个特定的查询来过滤和计算短链接的数量，并将结果转换为响应DTO列表
     * 
     * @param requestParam 分组ID列表，用于查询短链接数量
     * @return 返回一个列表，包含每个分组的短链接数量信息
     */
    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        // 构造查询条件，选择所需字段并设置查询条件
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        
        // 执行查询，获取短链接数量信息
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        
        // 将查询结果转换为响应DTO列表并返回
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Override
public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
    // 使用MurmurHash进行32位哈希计算
    int hash32 = MurmurHash.hash32(shortUri);
    // 根据哈希值计算模数，处理哈希值为负数的情况
    long mod = hash32 < 0 ? Integer.MAX_VALUE - (long) hash32 : hash32;

    // 从Redis中获取布隆过滤器的大小
    String bloomFilterSizeStr = stringRedisTemplate.opsForValue().get("sharding-expansion:bloom_filter_size");
    // 设置默认的布隆过滤器大小
    int bloomFilterSize = 1000;
    try {
        if (bloomFilterSizeStr != null) {
            bloomFilterSize = Integer.parseInt(bloomFilterSizeStr);
        }
    } catch (NumberFormatException ignored) {}

    // 计算布隆过滤器的索引位置
    long idx = mod % bloomFilterSize;

    // 获取当前布隆过滤器的索引
    String bloomFilterCurrentIndex = stringRedisTemplate.opsForValue().get("sharding-expansion:bloom_filter_current_index");
    if (bloomFilterCurrentIndex == null) {
        throw new RuntimeException("布隆过滤器分片索引未配置");
    }

    // 获取对应的布隆过滤器实例
    RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(
            String.format("sharding-expansion:short-link-create-bloom-filter-%s-%d", bloomFilterCurrentIndex, idx)
    );

    // 检查短链接是否存在于布隆过滤器中
    if (!bloomFilter.contains(shortUri)) {
        throw new ClientException("短链接不存在");
    }

    // 构建完整的短链接URL
    String fullShortUrl = buildFullShortUrl(request, shortUri);

    // 构建重定向键
    String gotoKey = String.format(GOTO_SHORT_LINK_KEY, fullShortUrl);
    // 从Redis中获取原始链接
    String originalLink = stringRedisTemplate.opsForValue().get(gotoKey);
    if (StrUtil.isNotBlank(originalLink)) {
        // 如果找到原始链接，则更新统计信息并重定向
        shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
        redirectTo(response, originalLink);
        return;
    }

    // 检查短链接是否已缓存穿透
    boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
    if (!contains) {
        redirectToNotFound(response);
        return;
    }

    // 检查是否已记录为空链接
    String isNullKey = String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl);
    String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(isNullKey);
    if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
        redirectToNotFound(response);
        return;
    }

    // 获取分布式锁
    RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
    try {
        if (!lock.tryLock(1, 30, TimeUnit.SECONDS)) {
            throw new RuntimeException("无法获取锁");
        }

        // 再次检查Redis中是否存在原始链接
        originalLink = stringRedisTemplate.opsForValue().get(gotoKey);
        if (StrUtil.isNotBlank(originalLink)) {
            shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            redirectTo(response, originalLink);
            return;
        }

        // 再次检查是否已记录为空链接
        gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(isNullKey);
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            redirectToNotFound(response);
            return;
        }

        // 查询数据库中的短链接记录
        LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
        ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
        if (shortLinkGotoDO == null) {
            stringRedisTemplate.opsForValue().set(isNullKey, "-", 30, TimeUnit.MINUTES);
            redirectToNotFound(response);
            return;
        }

        // 查询数据库中的短链接详细信息
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
        if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
            stringRedisTemplate.opsForValue().set(isNullKey, "-", 30, TimeUnit.MINUTES);
            redirectToNotFound(response);
            return;
        }

        // 将原始链接缓存到Redis中
        stringRedisTemplate.opsForValue().set(
                gotoKey,
                shortLinkDO.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
        );

        // 更新统计信息并重定向到原始链接
        shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
        redirectTo(response, shortLinkDO.getOriginUrl());
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取分布式锁失败", e);
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}


private String buildFullShortUrl(ServletRequest request, String shortUri) {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    String serverName = httpRequest.getServerName();
    int port = httpRequest.getServerPort();
    String portSuffix = port != 80 ? ":" + port : "";
    return serverName + portSuffix + "/" + shortUri;
}

private void redirectTo(ServletResponse response, String url) {
    try {
        ((HttpServletResponse) response).sendRedirect(url);
    } catch (IOException e) {
        throw new RuntimeException("重定向失败", e);
    }
}

private void redirectToNotFound(ServletResponse response) {
    redirectTo(response, "/page/notfound");
}


    /**
     * 构建短链接统计记录并设置用户信息
     * 
     * @param fullShortUrl 完整的短链接URL
     * @param request Servlet请求对象，用于获取cookies和请求头信息
     * @param response Servlet响应对象，用于添加cookie
     * @return 返回一个包含短链接统计信息的DTO对象
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        // 用于标记是否是UV（唯一访问者）首次访问
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 获取请求中的所有cookies
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        // 用于存储UV的值
        AtomicReference<String> uv = new AtomicReference<>();
        // 添加响应cookie的任务，用于在首次访问时设置UV cookie
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        // 如果存在cookies，尝试从中获取UV信息
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            // 如果不存在cookies，直接执行添加响应cookie的任务
            addResponseCookieTask.run();
        }
        // 获取请求的远程地址和其他相关信息
        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        // 将远程地址添加到Redis集合中，用于统计独立IP数量
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        // 构建并返回短链接统计记录DTO对象
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    /**
     * 处理短链统计信息的方法
     * 该方法接收一个短链统计记录DTO对象，将其转换为JSON字符串，
     * 并通过RocketMQ消息队列发送统计信息，以异步方式保存统计数据
     * 
     * @param statsRecord 短链统计记录的DTO对象，包含需要统计的信息
     */
    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO statsRecord) {
        // 创建一个HashMap用于存储统计记录的JSON字符串
        Map<String, String> producerMap = new HashMap<>();
        // 将statsRecord对象转换为JSON字符串并存入map中
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        // 消息队列为什么选用RocketMQ？详情查看：https://nageoffer.com/shortlink/question
        // 发送包含统计信息的消息到RocketMQ队列，进行异步处理
        shortLinkStatsSaveProducer.send(producerMap);
    }

    /**
     * 生成短链接的后缀
     * 此方法通过哈希算法生成唯一后缀，并使用布隆过滤器检查生成的后缀是否已存在
     * 如果后缀已存在，则重新生成，直到找到一个唯一的后缀
     * 
     * @param requestParam 原始链接和其他参数的封装对象
     * @return 生成的短链接后缀
     * @throws ServiceException 如果生成短链接的尝试超过10次，则抛出服务异常
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        // 记录自定义生成短链接的尝试次数
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            // 如果尝试次数超过10次，抛出异常，防止生成短链接过于频繁
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            // 获取原始URL，并追加UUID以生成不同的哈希值
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            // 使用哈希算法将原始URL转换为Base62编码的短链接后缀
            // 解决短链接哈希算法生成冲突问题的方法是通过在原始URL上追加UUID以确保唯一性
            shorUri = HashUtil.hashToBase62(originUrl);
            // 检查生成的短链接后缀是否已存在于布隆过滤器中
            // 不使用Set结构的原因是布隆过滤器在空间效率上更有优势，尽管会有一定的误判率
            // 如果布隆过滤器失效，其中的数据会丢失，但可以通过重新启动服务或重新生成短链接来恢复
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shorUri)) {
                // 如果生成的短链接后缀是唯一的，跳出循环
                break;
            }
            // 如果生成的短链接后缀已存在，增加尝试次数，重新生成
            customGenerateCount++;
        }
        // 返回生成的短链接后缀
        return shorUri;
    }

    /**
     * 通过锁机制生成短链接的唯一后缀
     * 该方法旨在防止短链接重复生成，通过在原始URL上添加随机UUID并使用哈希算法来创建唯一的后缀
     * 如果在多次尝试后仍然生成重复的短链接，则抛出异常
     * 
     * @param requestParam 短链接创建请求的DTO对象，包含创建短链接所需的信息
     * @return 返回生成的短链接后缀
     * @throws ServiceException 当短链接生成尝试超过10次仍失败时抛出此异常
     */
    private String generateSuffixByLock(ShortLinkCreateReqDTO requestParam) {
        // 初始化自定义生成计数器
        int customGenerateCount = 0;
        String shorUri;
        // 无限循环直到生成一个唯一的短链接后缀
        while (true) {
            // 如果生成次数超过10次，抛出异常
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            // 获取原始URL并添加UUID以确保唯一性
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            // 使用哈希算法将修改后的URL转换为短链接后缀
            // 解决短链接哈希算法生成冲突问题的方法见：https://nageoffer.com/shortlink/question
            shorUri = HashUtil.hashToBase62(originUrl);
            
            // 查询数据库以检查生成的短链接是否已存在
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, createShortLinkDefaultDomain + "/" + shorUri)
                    .eq(ShortLinkDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            
            // 如果短链接不存在，则退出循环
            if (shortLinkDO == null) {
                break;
            }
            // 如果短链接已存在，增加计数器并重新尝试生成
            customGenerateCount++;
        }
        // 返回生成的短链接后缀
        return shorUri;
    }

    /**
     * 根据给定的URL获取网站的Favicon图标URL
     * 该方法首先通过HTTP请求获取网站的HTML内容，然后使用Jsoup解析HTML以找到Favicon的链接
     * 如果找到Favicon链接，则返回其绝对URL；否则，返回null
     * 
     * @param url 网站的URL，用于获取Favicon
     * @return Favicon的URL，如果找不到则返回null
     * @SneakyThrows 用于处理可能抛出的IO异常，避免显式地处理或声明异常
     */
    @SneakyThrows
    private String getFavicon(String url) {
        // 创建URL对象，用于打开连接
        URL targetUrl = new URL(url);
        // 打开到目标URL的连接，并转换为HttpURLConnection类型
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        // 设置请求方法为GET
        connection.setRequestMethod("GET");
        // 建立实际的连接
        connection.connect();
        // 获取响应码
        int responseCode = connection.getResponseCode();
        // 检查响应码是否为HTTP_OK（200）
        if (HttpURLConnection.HTTP_OK == responseCode) {
            // 使用Jsoup连接URL并获取HTML文档
            Document document = Jsoup.connect(url).get();
            // 在文档中查找Favicon链接，选择第一个匹配的元素
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            // 如果找到Favicon链接，返回其绝对URL
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        // 如果没有找到Favicon链接，返回null
        return null;
    }

    /**
     * 验证跳转链接是否在白名单中
     * 此方法用于检查给定的URL是否属于配置好的域名白名单之内，以防止恶意跳转
     * 
     * @param originUrl 原始URL，需要验证的跳转链接
     * @throws ClientException 如果域名不在白名单中或域名提取失败，则抛出此异常
     */
    private void verificationWhitelist(String originUrl) {
        // 获取白名单启用状态
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        // 如果白名单功能未启用或启用标志为false，则直接返回，无需验证
        if (enable == null || !enable) {
            return;
        }
        
        // 从跳转链接中提取域名
        String domain = LinkUtil.extractDomain(originUrl);
        // 如果提取的域名为空或无效，则抛出异常提示跳转链接填写错误
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        
        // 获取白名单详细配置
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        // 如果提取的域名不在白名单中，则抛出异常提示不允许跳转，并建议生成特定网站的跳转链接
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
